package com.accesslimit.demo;

import java.time.LocalTime;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 滑动窗口限流法
 *
 * @author sam
 * @description 因为固定窗口算法存在窗口变界问题导致结果不精准，所以滑动窗口根据当前时间动态地切换当前限流的一个根据的时间范围
 * 但还是没解决窗口变界问题，某种意义说，只要有时间窗口的概念在，这个问题就永远避免不了...当滑动比固定的方法，结果上有更准一点
 */
public class RateLimiterSlidingWindow {

    // 阈值
    private Integer qps = 2;

    // 时间窗口范围大小
    private long windowSize = 1000;

    // 子窗口个数
    private Integer windowCount = 10;

    // 窗口列表
    private WindowInfo[] windowArray = new WindowInfo[windowCount];

    public RateLimiterSlidingWindow(int qps) {
        this.qps = qps;
        long currentTimeMillis = System.currentTimeMillis();
        for (int i = 0; i < windowArray.length; i++) {
            windowArray[i] = new WindowInfo(currentTimeMillis, new AtomicInteger(0));
        }
    }

    /**
     * 校验是否限流
     *
     * @return boolean
     * @description true:不限流 false:限流
     */
    public synchronized boolean tryAcquire() {
        long currentTimeMillis = System.currentTimeMillis();
        // 计算当前时间窗口
        int currentIndex = (int) (currentTimeMillis % windowSize / (windowSize / windowCount));
        // 更新当前窗口计数 & 重置过期窗口计数
        int sum = 0;
        for (int i = 0; i < windowArray.length; i++) {
            WindowInfo windowInfo = windowArray[i];
            // 超过时间窗口大小，重置计数器和开始时间
            if ((currentTimeMillis - windowInfo.getTime()) > windowSize) {
                windowInfo.getNumber().set(0);
                windowInfo.setTime(currentTimeMillis);
            }
            if (currentIndex == i && windowInfo.getNumber().get() < qps) {
                windowInfo.getNumber().incrementAndGet();
            }
            sum = sum + windowInfo.getNumber().get();
        }
        // 校验当前QPS是否超过限制
        return sum <= qps;
    }

    /**
     * 窗口信息
     */
    private class WindowInfo {
        // 窗口开始时间
        private Long time;

        // 计数器
        private AtomicInteger number;

        public WindowInfo(long time, AtomicInteger number) {
            this.time = time;
            this.number = number;
        }

        public Long getTime() {
            return time;
        }

        public void setTime(Long time) {
            this.time = time;
        }

        public AtomicInteger getNumber() {
            return number;
        }

        public void setNumber(AtomicInteger number) {
            this.number = number;
        }
    }


    /**
     * 测试主方法
     */
    public static void main(String[] args) throws InterruptedException {
        int qps = 2;
        int count = 20;
        int sleep = 300;
        int success = count * sleep / 1000 * qps;

        System.out.println("测试前准备数据和预期成功数：" +
                "\nqps -> " + qps + "\ncount -> " + count + "\nsleep -> " + sleep + "\nsuccess -> " + success);

        RateLimiterSlidingWindow rateLimiterSlidingWindow = new RateLimiterSlidingWindow(qps);

        int actualSuccess = 0;
        for (int i = 0; i < count; i++) {
            Thread.sleep(sleep);
            if (!rateLimiterSlidingWindow.tryAcquire()) {
                System.out.println(LocalTime.now() + ": " + "该请求已被限流！");
            } else {
                actualSuccess++;
                System.out.println(LocalTime.now() + ": " + "该请求已通过");
            }
        }

        System.out.println("测试后结果数据和实际成功数：" +
                "\nqps -> " + qps + "\ncount -> " + count + "\nsleep -> " + sleep + "\nsuccess -> " + actualSuccess);
    }
}
